#!/usr/bin/env python3
"""
GitLab批量分支合并脚本
用于在指定分组下的项目中，将存在的指定分支合并到目标分支
如果存在合并冲突，取消合并并输出详细日志
"""

import os
import sys
import json
import argparse
import logging
from typing import List, Dict, Optional, Tuple
import requests
from urllib.parse import quote


class GitLabBatchBranchMerger:
    def __init__(self, gitlab_url: str, access_token: str):
        self.gitlab_url = gitlab_url.rstrip('/')
        self.access_token = access_token
        self.headers = {
            'Authorization': f'Bearer {access_token}',
            'Content-Type': 'application/json'
        }
        self.session = requests.Session()
        self.session.headers.update(self.headers)
        
        # 设置日志
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s - %(levelname)s - %(message)s',
            handlers=[
                logging.FileHandler('batch_branch_merger.log'),
                logging.StreamHandler(sys.stdout)
            ]
        )
        self.logger = logging.getLogger(__name__)

    def search_projects_in_groups(self, groups: List[str], prefix: Optional[str] = None) -> List[Dict]:
        """在指定分组中搜索项目"""
        if not groups:
            raise ValueError("必须指定至少一个分组")
        
        self.logger.info(f"在分组中搜索项目: {', '.join(groups)}")
        if prefix:
            self.logger.info(f"项目前缀过滤: '{prefix}'")
        
        all_projects = []
        
        for group in groups:
            group_projects = self._get_projects_from_group(group, prefix)
            if group_projects:
                all_projects.extend(group_projects)
            else:
                self.logger.warning(f"分组 '{group}' 中未找到匹配的项目")
        
        # 去重（基于项目ID）
        unique_projects = {}
        for project in all_projects:
            unique_projects[project['id']] = project
        
        projects = list(unique_projects.values())
        self.logger.info(f"找到 {len(projects)} 个匹配的项目")
        return projects
    
    def _get_projects_from_group(self, group: str, prefix: Optional[str] = None) -> List[Dict]:
        """从指定分组获取项目，可选按前缀过滤"""
        projects = []
        page = 1
        per_page = 100
        
        try:
            # 首先获取分组ID
            group_url = f"{self.gitlab_url}/api/v4/groups/{quote(group, safe='')}"
            group_response = self.session.get(group_url)
            
            if group_response.status_code == 404:
                self.logger.warning(f"分组 '{group}' 不存在")
                return []
            
            group_response.raise_for_status()
            group_info = group_response.json()
            group_id = group_info['id']
            
            self.logger.debug(f"获取分组 '{group}' (ID: {group_id}) 中的项目")
            
            # 获取分组内的项目
            while True:
                params = {
                    'page': page,
                    'per_page': per_page,
                    'simple': True,
                    'include_subgroups': True  # 包含子分组
                }
                
                url = f"{self.gitlab_url}/api/v4/groups/{group_id}/projects"
                response = self.session.get(url, params=params)
                response.raise_for_status()
                
                page_projects = response.json()
                if not page_projects:
                    break
                
                if prefix:
                    # 过滤出匹配前缀的项目
                    filtered_projects = [
                        p for p in page_projects 
                        if p['name'].startswith(prefix) or p['path'].startswith(prefix)
                    ]
                    projects.extend(filtered_projects)
                else:
                    # 所有项目
                    projects.extend(page_projects)
                
                page += 1
                
        except requests.exceptions.RequestException as e:
            self.logger.error(f"在分组 '{group}' 中获取项目时出错: {e}")
            
        self.logger.debug(f"在分组 '{group}' 中找到 {len(projects)} 个项目")
        return projects

    def check_branch_exists(self, project_id: int, branch_name: str) -> bool:
        """检查项目中是否存在指定分支"""
        try:
            url = f"{self.gitlab_url}/api/v4/projects/{project_id}/repository/branches/{quote(branch_name, safe='')}"
            response = self.session.get(url)
            
            if response.status_code == 200:
                return True
            elif response.status_code == 404:
                return False
            else:
                self.logger.warning(f"检查分支 '{branch_name}' 时返回异常状态码: {response.status_code}")
                return False
                
        except requests.exceptions.RequestException as e:
            self.logger.error(f"检查分支 '{branch_name}' 时出错: {e}")
            return False

    def get_merge_conflicts(self, project_id: int, source_branch: str, target_branch: str) -> Tuple[bool, List[str]]:
        """检查两分支间是否存在合并冲突"""
        try:
            # 使用GitLab API的比较功能来检查是否有冲突
            url = f"{self.gitlab_url}/api/v4/projects/{project_id}/repository/compare"
            params = {
                'from': target_branch,
                'to': source_branch,
                'straight': True
            }
            
            response = self.session.get(url, params=params)
            response.raise_for_status()
            
            compare_data = response.json()
            
            # 如果没有差异，则无需合并
            if not compare_data.get('diffs'):
                return False, []
            
            # 尝试创建合并请求来检测冲突
            merge_url = f"{self.gitlab_url}/api/v4/projects/{project_id}/merge_requests"
            merge_data = {
                'source_branch': source_branch,
                'target_branch': target_branch,
                'title': f'[AUTO] Test merge {source_branch} to {target_branch}',
                'description': 'Automatic test merge request to detect conflicts'
            }
            
            merge_response = self.session.post(merge_url, json=merge_data)
            
            if merge_response.status_code == 201:
                mr_data = merge_response.json()
                mr_iid = mr_data['iid']
                
                # 检查合并请求状态
                mr_url = f"{self.gitlab_url}/api/v4/projects/{project_id}/merge_requests/{mr_iid}"
                mr_info_response = self.session.get(mr_url)
                mr_info_response.raise_for_status()
                mr_info = mr_info_response.json()
                
                # 删除测试合并请求
                self.session.delete(mr_url)
                
                # 检查是否有冲突
                if mr_info.get('merge_status') == 'cannot_be_merged':
                    conflicts = mr_info.get('conflicts', [])
                    return True, conflicts
                else:
                    return False, []
            else:
                # 如果无法创建合并请求，可能是因为没有差异或其他原因
                self.logger.warning(f"无法创建测试合并请求: {merge_response.text}")
                return False, []
                
        except requests.exceptions.RequestException as e:
            self.logger.error(f"检查合并冲突时出错: {e}")
            return False, []

    def merge_branch(self, project_id: int, source_branch: str, target_branch: str, 
                    commit_message: Optional[str] = None) -> Tuple[bool, str]:
        """将源分支合并到目标分支"""
        try:
            # 首先检查是否有冲突
            has_conflicts, conflicts = self.get_merge_conflicts(project_id, source_branch, target_branch)
            
            if has_conflicts:
                conflict_files = [c.get('new_path', 'unknown') for c in conflicts] if conflicts else ['检测到冲突但无法获取具体文件']
                return False, f"存在合并冲突，涉及文件: {', '.join(conflict_files)}"
            
            # 创建合并请求
            merge_url = f"{self.gitlab_url}/api/v4/projects/{project_id}/merge_requests"
            
            if not commit_message:
                commit_message = f"Merge branch '{source_branch}' into '{target_branch}'"
            
            merge_data = {
                'source_branch': source_branch,
                'target_branch': target_branch,
                'title': commit_message,
                'description': f'Automatic merge of {source_branch} into {target_branch}'
            }
            
            merge_response = self.session.post(merge_url, json=merge_data)
            
            if merge_response.status_code == 201:
                mr_data = merge_response.json()
                mr_iid = mr_data['iid']
                
                # 立即接受合并请求
                accept_url = f"{self.gitlab_url}/api/v4/projects/{project_id}/merge_requests/{mr_iid}/merge"
                accept_data = {
                    'merge_commit_message': commit_message,
                    'should_remove_source_branch': False  # 不删除源分支
                }
                
                accept_response = self.session.put(accept_url, json=accept_data)
                
                if accept_response.status_code == 200:
                    return True, "合并成功"
                else:
                    # 删除创建的合并请求
                    self.session.delete(f"{self.gitlab_url}/api/v4/projects/{project_id}/merge_requests/{mr_iid}")
                    return False, f"接受合并请求失败: {accept_response.text}"
            else:
                return False, f"创建合并请求失败: {merge_response.text}"
                
        except requests.exceptions.RequestException as e:
            return False, f"合并过程中出错: {e}"

    def batch_merge_branches(self, source_branch: str, target_branch: str, 
                           groups: List[str], prefix: Optional[str] = None,
                           commit_message: Optional[str] = None, dry_run: bool = False) -> Dict:
        """批量合并分支"""
        self.logger.info(f"开始批量合并分支: '{source_branch}' -> '{target_branch}'")
        
        if commit_message:
            self.logger.info(f"使用自定义提交信息: {commit_message}")
        
        if dry_run:
            self.logger.info("*** 试运行模式 - 不会实际执行合并 ***")
        
        projects = self.search_projects_in_groups(groups, prefix)
        
        if not projects:
            self.logger.warning("没有找到匹配的项目")
            return {'success': 0, 'failed': 0, 'skipped': 0, 'total': 0}
        
        results = {
            'success': 0,
            'failed': 0,
            'skipped': 0,
            'total': len(projects),
            'details': []
        }
        
        for project in projects:
            project_id = project['id']
            project_name = project['name']
            project_path = project['path_with_namespace']
            
            self.logger.info(f"处理项目: {project_name} ({project_path})")
            
            # 检查源分支是否存在
            if not self.check_branch_exists(project_id, source_branch):
                self.logger.info(f"项目 {project_name} 中不存在源分支 '{source_branch}'，跳过")
                results['skipped'] += 1
                results['details'].append({
                    'project': project_name,
                    'project_path': project_path,
                    'status': 'skipped',
                    'reason': f"源分支 '{source_branch}' 不存在"
                })
                continue
            
            # 检查目标分支是否存在
            if not self.check_branch_exists(project_id, target_branch):
                self.logger.warning(f"项目 {project_name} 中不存在目标分支 '{target_branch}'，跳过")
                results['skipped'] += 1
                results['details'].append({
                    'project': project_name,
                    'project_path': project_path,
                    'status': 'skipped',
                    'reason': f"目标分支 '{target_branch}' 不存在"
                })
                continue
            
            if not dry_run:
                success, message = self.merge_branch(project_id, source_branch, target_branch, commit_message)
                if success:
                    self.logger.info(f"项目 {project_name} 合并成功")
                    results['success'] += 1
                    results['details'].append({
                        'project': project_name,
                        'project_path': project_path,
                        'status': 'success',
                        'message': message
                    })
                else:
                    self.logger.error(f"项目 {project_name} 合并失败: {message}")
                    results['failed'] += 1
                    results['details'].append({
                        'project': project_name,
                        'project_path': project_path,
                        'status': 'failed',
                        'reason': message
                    })
            else:
                self.logger.info(f"[试运行] 将在项目 {project_name} 中合并 '{source_branch}' -> '{target_branch}'")
                results['success'] += 1
                results['details'].append({
                    'project': project_name,
                    'project_path': project_path,
                    'status': 'dry_run',
                    'message': f"计划合并 {source_branch} -> {target_branch}"
                })
        
        return results


def load_config(config_file: str) -> Dict:
    """加载配置文件"""
    try:
        with open(config_file, 'r', encoding='utf-8') as f:
            return json.load(f)
    except FileNotFoundError:
        print(f"配置文件 {config_file} 不存在")
        return {}
    except json.JSONDecodeError as e:
        print(f"配置文件格式错误: {e}")
        return {}


def main():
    parser = argparse.ArgumentParser(description='GitLab批量分支合并工具')
    parser.add_argument('--gitlab-url', help='GitLab服务器URL')
    parser.add_argument('--token', help='GitLab访问令牌')
    parser.add_argument('--source-branch', required=True, help='源分支名称（要合并的分支）')
    parser.add_argument('--target-branch', required=True, help='目标分支名称（合并到的分支）')
    parser.add_argument('--groups', nargs='+', required=True, help='GitLab分组名称（可指定多个）')
    parser.add_argument('--prefix', help='项目名称前缀过滤')
    parser.add_argument('--commit-message', help='自定义合并提交信息')
    parser.add_argument('--dry-run', action='store_true', help='试运行模式，不实际执行合并')
    parser.add_argument('--config', help='配置文件路径')
    
    args = parser.parse_args()
    
    # 如果提供了配置文件，从中加载配置
    config = {}
    if args.config:
        config = load_config(args.config)
    
    # 命令行参数优先于配置文件
    gitlab_url = args.gitlab_url or config.get('gitlab_url')
    token = args.token or config.get('access_token') or os.getenv('GITLAB_TOKEN')
    
    if not gitlab_url or not token:
        print("错误: 必须提供GitLab URL和访问令牌")
        sys.exit(1)
    
    merger = GitLabBatchBranchMerger(gitlab_url, token)
    
    try:
        results = merger.batch_merge_branches(
            source_branch=args.source_branch,
            target_branch=args.target_branch,
            groups=args.groups,
            prefix=args.prefix,
            commit_message=args.commit_message,
            dry_run=args.dry_run
        )
        
        print(f"\n批量分支合并完成:")
        print(f"总项目数: {results['total']}")
        print(f"成功: {results['success']}")
        print(f"失败: {results['failed']}")
        print(f"跳过: {results['skipped']}")
        
        if results['failed'] > 0:
            print("\n失败详情:")
            for detail in results['details']:
                if detail['status'] == 'failed':
                    print(f"  - {detail['project']} ({detail['project_path']}): {detail['reason']}")
        
        if results['skipped'] > 0:
            print("\n跳过详情:")
            for detail in results['details']:
                if detail['status'] == 'skipped':
                    print(f"  - {detail['project']} ({detail['project_path']}): {detail['reason']}")
        
    except KeyboardInterrupt:
        print("\n操作被用户中断")
        sys.exit(1)
    except Exception as e:
        print(f"执行过程中出现错误: {e}")
        sys.exit(1)


if __name__ == '__main__':
    main()